package com.tinywind.admin.common.config.shiro;

import com.tinywind.admin.common.config.shiro.filter.AuthcFilter;
import com.tinywind.admin.common.config.shiro.filter.KickoutControlFilter;
import com.tinywind.admin.common.utils.idgen.IdGenerate;
import io.lettuce.core.api.StatefulRedisConnection;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.*;
import org.crazycake.shiro.serializer.FstSerializer;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author liuxingyu01
 * @date 2021-01-07-22:10
 * @description shiro配置
 **/
@Configuration
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private Integer redisPort;

    @Value("${spring.redis.password}")
    private String password;


    /**
     * 地址过滤器
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定义过滤器
        Map<String, Filter> filtersMap = new LinkedHashMap<>();
        filtersMap.put("kickout", kickoutControlFilter());
        filtersMap.put("authc", new AuthcFilter());

        shiroFilterFactoryBean.setFilters(filtersMap);

        // 这里必须用LinkedHashMap，因为可HashMap是无序的，可能到filter里面顺序就变了
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的url，顺序判断
        // 开放登录接口
        filterChainDefinitionMap.put("/system/login", "anon");
        // 开放获取图形验证码接口
        filterChainDefinitionMap.put("/system/getCaptcha", "anon");
        // 开放itfc和anon接口（itfc和anon开头的可免shiro认证访问）
        filterChainDefinitionMap.put("/anon/**", "anon");
        filterChainDefinitionMap.put("/itfc/**", "anon");

        // 开放接口文档(swagger-ui)
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/service-worker.js", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        // 开放静态文件
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/lib/**", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");
        // 其余url全部拦截，必须放在最后
        filterChainDefinitionMap.put("/**", "kickout,authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义的session管理
        securityManager.setSessionManager(sessionManager());
        // 设置自定义的cache管理
        securityManager.setCacheManager(redisCacheManager());
        // 设置自定义的relam
        securityManager.setRealm(loginRelam());
        return securityManager;
    }

    /**
     * 身份验证器
     */
    @Bean
    public LoginRelam loginRelam() {
        LoginRelam loginRelam = new LoginRelam();
        loginRelam.setCredentialsMatcher(hashedCredentialsMatcher());
        // loginRelam.setCacheManager(redisCacheManager()); // 把它移动到SecurityManager里面了，好像没什么区别
        return loginRelam;
    }

    /**
     * 以下是为了能够使用@RequiresPermission()等标签
     * shiro注解:
     * <p>
     * RequiresGuest 只有游客可以访问
     * RequiresAuthentication 需要登录才能访问---->可以直接添加到类上
     * RequiresUser 已登录的用户或“记住我”的用户能访问
     * RequiresRoles 已登录的用户需具有指定的角色才能访问
     * RequiresPermissions 已登录的用户需具有指定的权限才能访问
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启shiro aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 缓存管理器
     * 用于往Redis存储权限和角色标识
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setExpire(1800);
        redisCacheManager.setPrincipalIdFieldName("userId");
        return redisCacheManager;
    }


    /**
     * 配置Redis管理器(使用lettuce连接池)
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public IRedisManager redisManager() {
        // 创建lettuce连接池，和application.properties里配置的一样
        GenericObjectPoolConfig<StatefulRedisConnection<byte[], byte[]>> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(20);
        poolConfig.setMaxWaitMillis(5000);
        poolConfig.setMinIdle(5);

//        LettuceRedisManager redisManager = new LettuceRedisManager(redisHost, redisPort);
//        redisManager.setPassword(password);
//        redisManager.setTimeout(5); // 单位秒
//        redisManager.setPoolConfig(poolConfig);

        LettuceRedisManager redisManager = LettuceRedisManager.builder()
                .host(redisHost)
                .port(redisPort)
                .password(password)
                .timeout(5)
                .poolConfig(poolConfig)
                .build();

        return redisManager;
    }


    /**
     * 配置RedisSessionDAO
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setExpire(1800);
        redisSessionDAO.setRedisManager(redisManager());
        // 缓存到localThread里面，避免每shiro频繁请求redis的问题
        redisSessionDAO.setSessionInMemoryEnabled(true);
        redisSessionDAO.setSessionInMemoryTimeout(1000);
        // custtom session value serializer, defaule is jdk serializer
        redisSessionDAO.setValueSerializer(new FstSerializer());
        // 自定义session-id的生成规则
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        return redisSessionDAO;
    }

    /**
     * Session管理器
     *
     * @return
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setGlobalSessionTimeout(1000 * 1800);// 会话过期时间，单位：毫秒(在无操作时开始计时)->一1800秒
        // 去掉shiro登录时url里的JSESSIONID(不配置也可以，默认的就是false)
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        // 指定sessionid
        sessionManager.setSessionIdCookie(sessionIdCookie());
        // 允许shiro的sessionId放在cookie中(默认是启用，false为禁用)
        sessionManager.setSessionIdCookieEnabled(true);
        // 删除无效session(默认就是true)
        sessionManager.setDeleteInvalidSessions(true);

        return sessionManager;
    }


    /**
     * 可能存在的问题：后面遇到的话，可以去掉这个，不用这个了
     * 指定本系统sessionid, 问题: 与servlet容器名冲突, 如jetty, tomcat 等默认jsessionid,
     * 当跳出shiro servlet时如error-page容器会为jsessionid重新分配值导致登录会话丢失!
     *
     * @return
     */
    public SimpleCookie sessionIdCookie() {
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setHttpOnly(true); // 不会暴露给客户端
        simpleCookie.setMaxAge(-1); // 设置Cookie的过期时间，秒为单位，默认-1表示关闭浏览器时过期Cookie
        simpleCookie.setName("session_token");
        return simpleCookie;
    }


    /**
     * 自定义sessionid的生成方式，这里改成没有-的UUID（shiro默认的是带-的UUID）
     *
     * @return
     */
    public SessionIdGenerator sessionIdGenerator() {
        SessionIdGenerator sessionIdGenerator = new SessionIdGenerator() {
            @Override
            public Serializable generateId(Session session) {
                return "shiro-" + IdGenerate.uuid();
            }
        };
        return sessionIdGenerator;
    }


    /**
     * 凭证匹配器
     * 执行login(token)后由securityManager调用，用于计算密码加密后的密文
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 设置散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-256");
        // 设置散列计算次数，相当于md5(md5(""))
        hashedCredentialsMatcher.setHashIterations(2);
        // storedCredentialsHexEncoded默认是true，此时用的是密码加密用的是Hex编码；false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }


    /**
     * 限制同一账号登录同时登录人数控制
     *
     * @return
     */
    @Bean
    public KickoutControlFilter kickoutControlFilter() {
        KickoutControlFilter kickoutControlFilter = new KickoutControlFilter();
        kickoutControlFilter.setCacheManager(redisCacheManager());
        kickoutControlFilter.setSessionManager(sessionManager());
        kickoutControlFilter.setKickoutAfter(false);
        kickoutControlFilter.setMaxSession(1);
        return kickoutControlFilter;
    }
}
